package com.mini;

import javax.sql.DataSource;
import java.sql.*;
import java.util.Arrays;

public abstract class MiniAbstractQueryRunner {

    private volatile boolean pmdKnownBroken = false;
    // 使用final定义的全局变量，必须在类中的无参和有参构造中同时都有初始化值才能正常编译
    private final DataSource dataSource;

    public MiniAbstractQueryRunner() {
        dataSource = null;
    }

    // 先写一个具备所有参数的有参构造，其余的有参构造都来调用该构造函数；可以提高代码的复用性和维护性
    public MiniAbstractQueryRunner (Boolean pmdKnownBroken, DataSource ds) {
        this.pmdKnownBroken = pmdKnownBroken;
        this.dataSource = ds;
    }
    public MiniAbstractQueryRunner (Boolean pmdKnownBroken) {
        this(pmdKnownBroken,null);
    }
    public MiniAbstractQueryRunner (DataSource ds) {
        this(false,ds);
    }

    // 从数据源中获取一个连接
    protected Connection prepareConnection() throws SQLException {
        if (dataSource == null) {
            throw new SQLException("以该方法来获取连接需要一个数据源");
        }
        return dataSource.getConnection();
    }

    // 抑制传递的参数为空时的任何异常（当方法中提供了无效参数时，方便关闭对象），并提供给子类重写的权利
    protected void close(Connection conn) throws SQLException {
        MiniDbUtils.close(conn);
    }
    protected void close(Statement stmt) throws SQLException {
        MiniDbUtils.close(stmt);
    }
    protected void close(ResultSet rs) throws SQLException {
        MiniDbUtils.close(rs);
    }

    /* 创建一个预处理对象
     * PreparedStatement SQL语句被预编译并储存到该对象中
     * 因为是方法，而且是protected修饰符，所以就给了子类以重写的机会来改变父类创建此对象的方式 */
    protected PreparedStatement prepareStatement(Connection conn, String sql) throws SQLException {
        return conn.prepareStatement(sql);
    }
    protected PreparedStatement prepareStatement(Connection conn, String sql, int returnedKeys) throws SQLException {
        return conn.prepareStatement(sql,returnedKeys);
    }

    protected void rethrow(SQLException cause, String sql, Object... params) throws SQLException {

        String causeMessage = cause.getMessage();
        if (causeMessage == null) {
            causeMessage = "";
        }
        StringBuffer msg = new StringBuffer(causeMessage);

        msg.append(" Query: ");
        msg.append(sql);
        msg.append(" Parameters: ");

        if (params == null) {
            msg.append("[]");
        } else {
            msg.append(Arrays.deepToString(params));
        }

        SQLException e = new SQLException(msg.toString(), cause.getSQLState(), cause.getErrorCode());
        e.setNextException(cause);

        throw e;
    }
    // 将不参数的SQL预编对象填充成一个完整的包含参数的SQL预编对象
    public void fillStatement(PreparedStatement stmt, Object... params) throws SQLException {
        /* 如果能的话就检查参数和预处理语句中问号（?）的数量
         * ParameterMetaData 元数据。封装了参数的数量和类型 */
        ParameterMetaData pmd = null;
        if (!pmdKnownBroken) {
            try {
                // 获取元数据。检索预处理SQL中问号"?"的数量，并封装起来
                pmd = stmt.getParameterMetaData();
                if (pmd == null) {
                    pmdKnownBroken = true;
                } else {
                    // 获取预处理SQL语句中问号（?）的个数（或者说执行SQL语句所需参数的数量）
                    int stmtCount = pmd.getParameterCount();
                    // 获取可变参数的参数数量并对实际参数值的数量进行处理；没有值，数量就为0
                    int paramsCount = params == null ? 0 : params.length;
                    // 问号的数量与实际参数的数量不匹配就报错
                    if (stmtCount != paramsCount) {
                        throw new SQLException("错误的参数数量: 期望： "
                                + stmtCount + ", 实际提供了： " + paramsCount);
                    }
                }
            } catch (SQLFeatureNotSupportedException ex) {
                pmdKnownBroken = true;
            }
        }

        // 如果提供的参数为空就直接返回，表示这个预处理SQL是不需要填充参数的；储存的SQL语句是可以直接运行的
        if (params == null) {
            return;
        }

        // 将传递的参数填充到预处理SQL对象中，生成一个完整的预编SQL对象
        for (int i = 0; i < params.length; i++) {
            if (params[i] != null) {
                /* 给预编SQL的问号"?"赋值
                 * 其内部会通过java类型和JDBC类型的自动映射找到对应的数据库类型 */
                stmt.setObject(i + 1, params[i]);
            } else {
                /* 传递的参数允许使用null，但是对应的是数据库中的VARCHAR类型
                 * Types 用于标识JDBC类型的常量类，每个类型都对应一个数字 */
                int sqlType = Types.VARCHAR;
                if (!pmdKnownBroken) {
                    try {
                        // 如果JAVA的元数据中有适配类型，就用该JDBC类型映射
                        sqlType = pmd.getParameterType(i + 1);
                    } catch (SQLException e) {
                        pmdKnownBroken = true;
                    }
                }
                // 处理java的特殊类型null，对应JDBC的VARCHAR类型数据
                stmt.setNull(i + 1, sqlType);
            }
        }
    }

    // 提供get方法
    public DataSource getDataSource () {return dataSource;}
    public boolean getPmdKnownBroken () {return pmdKnownBroken;}
}
